Custom SRP笔记

Posted by FlowingCrescent on 2021-06-19
Estimated Reading Time 12 Minutes
Words 3k In Total
Viewed Times

本文为个人学习Catlike Coding的教程系列Custom SRP时的学习笔记

Custom Render Pipeline - Taking Control of Rendering

搭建了渲染不透明、天空盒、半透明物体以及错误渲染物体的基本渲染管线
此时会发现调用到的API与RenderFeature中的相差无几,或许RenderFeature其实就是作为管线的补充内容。

使用C#的partial关键字对类进行分割

catlike使用partial关键字将CameraRenderer类进行了拆分,将Editor模式时才运行的功能拆分了出去,
然后使用#if UNITY_EDITOR以及#endif包含了Editor模式时才运行的代码。
具体可参考官方文档

一次DrawRenderers可包含多种ShaderTagId的物体

写法如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
    static ShaderTagId[] legacyShaderTagIds = {
new ShaderTagId("Always"),
new ShaderTagId("ForwardBase"),
new ShaderTagId("PrepassBase"),
new ShaderTagId("Vertex"),
new ShaderTagId("VertexLMRGBM"),
new ShaderTagId("VertexLM")
};


//.....

var drawingSettings = new DrawingSettings(
legacyShaderTagIds[0], new SortingSettings(camera)
)
{
overrideMaterial = errorMaterial
};

for (int i = 1; i < legacyShaderTagIds.Length; i++)
{
drawingSettings.SetShaderPassName(i, legacyShaderTagIds[i]);
}

var filteringSettings = FilteringSettings.defaultValue;
context.DrawRenderers(
cullingResults, ref drawingSettings, ref filteringSettings
);


就是给DrawingSetting调用SetShaderPassName以增加更多的Pass种类
image.png

这节做完时的效果

2021.06.18


Draw Calls - Shaders and Batches

这一节主要是写Shader以及实现批处理,是个人比较熟悉的内容,没什么特别好讲的,但其中有一处细节是第一次知道。
关于Unity各种批处理的详细内容也可以看此文:相关文档

Clear Buffer也是一次DrawCall

第一节中有提到如果Clear的位置在SetUpCameraProperties之前,则会比较低效地调用Shader进行Clear,因此本质上算是一次绘制,这也是比较好理解的
image.png

但之后的快速Clear也是作为一次DrawCall,这在第二节中被提及:
image.png
姑且作为一个小知识记下。

image.png

这节做完时的效果

2021.06.19


Directional Lights - Direct Illumination

这一节主要是实现Directional Light以及BRDF,也就是PBR的直接光照部分,大部分也是我比较熟悉的内容

关于法线插值与归一化

在fragment shader时需要对法线进行归一化也是老生常谈的基础内容,不过个人还是第一次可视化地直观看见artifact,还是挺有趣的:
image.png

NativeArray

Catlike使用NativeArray来储存灯光数据

It’s a struct that acts like an array, but provides a connection to a native memory buffer.** It makes it possible to efficiently share data between managed C# code and the native Unity engine code.**

简单来说就是相比普通的数组,它在C#与Unity底层代码之间传递效率更高。
NativeArray官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void SetupLights()
{
NativeArray<VisibleLight> visibleLights = cullingResults.visibleLights;
int dirLightCount = 0;
for (int i = 0; i < visibleLights.Length; i++)
{
VisibleLight visibleLight = visibleLights[i];
if (visibleLight.lightType == LightType.Directional)
{
SetupDirectionalLight(dirLightCount++,ref visibleLight);
if (dirLightCount >= maxDirLightCount)
{
break;
}
}
}

buffer.SetGlobalInt(dirLightCountId, visibleLights.Length);
buffer.SetGlobalVectorArray(dirLightColorsId, dirLightColors);
buffer.SetGlobalVectorArray(dirLightDirectionsId, dirLightDirections);
}

就用法上而言可以说跟数组相差无几

Shader GUI

其实这是个人第一次做Shader GUI相关的内容,做过一次之后发现其实跟Editor拓展编辑器极为神似,Unity在这方面还是挺方便的。
总之,在Shader结尾写上CustomEditor "CustomShaderGUI"
然后将CustomShaderGUI.cs放入Editor文件夹下表示是个Editor程序即可
最关键的就是调用OnGUI()函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public override void OnGUI(MaterialEditor materialEditor, MaterialProperty[] properties)
{
//绘制原有的材质GUI
base.OnGUI(materialEditor, properties);

//对自定义GUI所必要的变量进行赋值
editor = materialEditor;
materials = materialEditor.targets;
this.properties = properties;

EditorGUILayout.Space();
//嵌套选项
showPresets = EditorGUILayout.Foldout(showPresets, "Presets", true);
if (showPresets)
{
OpaquePreset();
ClipPreset();
FadePreset();
TransparentPreset();
}
}

其对Property的变量进行赋值的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool SetProperty(string name, float value)
{
MaterialProperty property = FindProperty(name, properties, false);
if (property != null)
{
property.floatValue = value;
return true;
}
return false;
}
void SetProperty(string name, string keyword, bool value)
{
if (SetProperty(name, value ? 1f : 0f))
{
SetKeyword(keyword, value);
}
}

是Keyword的就增加个Keyword值设置,同时还要将Property的float进行修改。

image.png

这节做完的效果

2021.06.21


Directional Shadows - Cascaded Shadow Maps

这一节内容相当多,阴影的原理虽然早就了解,但是实现起来还是比较繁琐复杂。

万能的CullingResults

在前几节的时候还觉得CullingResults类就是单纯的储存被裁剪后的物体的类,但后来发现这个类能做的事情相当多,且与Shadow Map的实现强相关。

1
2
3
4
5
cullingResults.ComputeDirectionalShadowMatricesAndCullingPrimitives(
light.visibleLightIndex, i, cascadeCount, ratios, tileSize,
light.nearPlaneOffset, out Matrix4x4 viewMatrix,
out Matrix4x4 projectionMatrix, out ShadowSplitData splitData
);

可通过它自带的ComputeDirectionalShadowMatricesAndCullingPrimitives()获取VP矩阵以及SplitData,可以说有这几项内容之后普通的ShadowMap基本就实现完了……

而这个cullingResults的获取也十分简单(取自CameraRenderer.cs):

1
2
3
4
5
6
7
8
bool Cull (float maxShadowDistance) {
if (camera.TryGetCullingParameters(out ScriptableCullingParameters p)) {
p.shadowDistance = Mathf.Min(maxShadowDistance, camera.farClipPlane);
cullingResults = context.Cull(ref p);
return true;
}
return false;
}

最后调用context.DrawShadows进行shadowmap的绘制:

1
2
3
4
5
6
7
8
9
var shadowSettings = new ShadowDrawingSettings(cullingResults, light.visibleLightIndex);

//......

shadowSettings.splitData = splitData;

//......

context.DrawShadows(ref shadowSettings);

可发现这里也还是有cullingResults的存在

Shadow Texture的一些特殊设置

Shadow Texture从它刚申请来的时候就是那么的与众不同:

1
2
3
4
buffer.GetTemporaryRT(
dirShadowAtlasId, atlasSize, atlasSize,
32, FilterMode.Bilinear, RenderTextureFormat.Shadowmap
);

奢华的32bit,还有Unity根据平台自适应的TextureFormat。

1
2
3
4
buffer.SetRenderTarget(
dirShadowAtlasId,
RenderBufferLoadAction.DontCare, RenderBufferStoreAction.Store
);

关于RenderBufferLoadAction

This enum describes what should be done on the render target when it is activated (loaded).
When the GPU starts rendering into a render target, this setting specifies the action that should be performed on the existing contents of the surface. Tile-based GPUs may get performance advantage if the load action is Clear or DontCare. The user should avoid using RenderBufferLoadAction.Load whenever possible.

Please note that not all platforms have load/store actions, so this setting might be ignored at runtime. Generally mobile-oriented graphics APIs (OpenGL ES, Metal) take advantage of these settings.

设置为DontCare就是说读取这个RenderBuffer时不对它的内容进行任何处理

Shadow Pancaking

由于ShadowCaster渲染时,相机的near plane会尽可能地靠近物体以提高精度,但这也可能导致有些物体的个别顶点比near plane更加靠近相机而被剔除
image.png

然而当物体本身就交错于near plane两边时,还是会存在artifact,这是就要将near plane进行后(向相机)移
image.png

Culling Bias

由于Cascade Shadow Map会在Shadow Texture上多次绘制同一物体,为了减少Draw Call,就尽可能剔除已经在较低层级的Cascade中绘制过了的物体,因此有了Culling Bias的概念。
image.png

image.png

这节做完的效果

2021.06.23

Baked Light - Light Maps and Probes

Light Probe Proxy Volumes

由于光照探针是逐物体使用(一个物体对应一个探针的信息),因此若物体过大,其间接光照信息便容易出错,此时便可以使用Light Probe Proxy Volumes,简称LPPVs。
image.png
image.png
image.png
但Catlike提到,此时探针只会使用L1SH的信息,因此会显得精度较低。这也是能够理解的,毕竟是动态更新,应当尽量减少计算量。

Meta Pass

因为间接漫射光应该受到这些物体表面的反射率的影响,因此需要新定义一个Meta Pass,用来补完烘焙间接光的颜色。

其中最重要的就是Fragment:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
float4 MetaPassFragment(Varyings input): SV_TARGET
{
float4 base = GetBase(input.baseUV);
Surface surface;
ZERO_INITIALIZE(Surface, surface);
surface.color = base.rgb;
surface.metallic = GetMetallic(input.baseUV);
surface.smoothness = GetSmoothness(input.baseUV);
BRDF brdf = GetBRDF(surface);
float4 meta = 0.0;
if (unity_MetaFragmentControl.x)
{
meta = float4(brdf.diffuse, 1.0);
meta.rgb += brdf.specular * brdf.roughness * 0.5;
meta.rgb = min(
PositivePow(meta.rgb, unity_OneOverOutputBoost), unity_MaxOutputValue
);
}
else if (unity_MetaFragmentControl.y)
{
meta = float4(GetEmission(input.baseUV), 1.0);
}
return meta;
}

其实与普通Pass的Frag没什么特别的区别

image.png

这节做完时的效果

2021.06.27

Shadow Masks - Baking Direct Occlusion

这一节内容较少,主要还是实现Shadow Mask以及Distance Shadow Mask

逻辑或运算符 |

CameraRenderer中有这么一段写法,我一直很在意其中"|"的用法:

1
2
3
4
5
6
7
8
9
10
11
12
var drawingSettings = new DrawingSettings(
unlitShaderTagId, sortingSettings
)
{
enableDynamicBatching = useDynamicBatching,
enableInstancing = useGPUInstancing,
perObjectData =
PerObjectData.Lightmaps | PerObjectData.ShadowMask |
PerObjectData.LightProbe | PerObjectData.OcclusionProbe |
PerObjectData.LightProbeProxyVolume |
PerObjectData.OcclusionProbeProxyVolume
};


PerObjectData是一个枚举类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
public enum PerObjectData
{
//
// 摘要:
// Do not setup any particular per-object data besides the transformation matrix.
None = 0,
//
// 摘要:
// Setup per-object light probe SH data.
LightProbe = 1,
//
// 摘要:
// Setup per-object reflection probe data.
ReflectionProbes = 2,
//
// 摘要:
// Setup per-object light probe proxy volume data.
LightProbeProxyVolume = 4,
//
// 摘要:
// Setup per-object lightmaps.
Lightmaps = 8,
//
// 摘要:
// Setup per-object light data.
LightData = 16,
//
// 摘要:
// Setup per-object motion vectors.
MotionVectors = 32,
//
// 摘要:
// Setup per-object light indices.
LightIndices = 64,
//
// 摘要:
// Setup per-object reflection probe index offset and count.
ReflectionProbeData = 128,
//
// 摘要:
// Setup per-object occlusion probe data.
OcclusionProbe = 256,
//
// 摘要:
// Setup per-object occlusion probe proxy volume data (occlusion in alpha channels).
OcclusionProbeProxyVolume = 512,
//
// 摘要:
// Setup per-object shadowmask.
ShadowMask = 1024
}

每个值都是2的n次方

官方文档中如此解释"|":
image.png
也就是说,对于c中每一个二进制位,如果ab两者中对应的二进制位有一个是1,则这个二进制位值即为1
如果再形象一些表达,就是一排电闸,每个电闸对应一盏灯,上面perObjectData的赋值其实就是打开对应灯的开关。
还是挺有意思的写法。

关于矢量值的通道选择

在进行多光源Shadowmask采样时需要对通道进行选择,Catlike此处使用float4 value[channel]的写法来获取对应的通道值,他也解释了为什么不是手动传入一个float4值来点乘获取(如获取r通道值则传入float4(1, 0, 0, 0)来dot)。
实际上value[channel]的写法,GPU会帮我们编译成之前所说的传入对应通道值为1的float4值进行点乘,如果我们还写成点乘,则还得额外设置一个float4数组,反而变得麻烦了。
image.png

image.png

这节做完时的效果

2021.06.29

LOD and Reflections - Adding Details

这节主要就是实现LOD和反射探针,内容也比较少。

LOD Bias

已知LOD是根据距离判断物体所占屏幕比例大小,来进行模型和贴图的切换以减少渲染开销。而LOD Bias则是对这一比例进行调整:

image.png

LOD Bias = 2.0(默认值)

image.png

LOD Bias = 1.0

可以看出其实就是单纯在比例上增加一个乘法。
如果更感性来说,就是LOD Bias越大,对设备性能要求越高,LOD Bias越低则设备性能要求越低。

LOD Cross Fade

由于单纯进行LOD模型的切换会导致跳变,导致观感较差,因此在切换时也要追求自然,便有了Cross Fade
Cross Fade其实就是对进行切换的两级LOD模型都进行渲染,此时在Shader里通过Clip来进行切换,写法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void ClipLOD (float2 positionCS, float fade) {
#if defined(LOD_FADE_CROSSFADE)
float dither = InterleavedGradientNoise(positionCS.xy, 0);
clip(fade + (fade < 0.0 ? dither : -dither));
#endif
}

float4 LitPassFragment(Varyings input): SV_TARGET
{
UNITY_SETUP_INSTANCE_ID(input);

ClipLOD(input.positionCS.xy, unity_LODFade.x);
//......
}

通过算法可以看出,正在消隐的LOD模型,fade值会大于0且不断减小,而正在出现的LOD模型fade值则小于零而不断增加。

倒是其中调用的InterleavedGradientNoise函数非常值得挖掘,它被定义在srp自带的Random.hlsl中:

1
2
3
4
5
6
7
8
9
//From  Next Generation Post Processing in Call of Duty: Advanced Warfare [Jimenez 2014]
// http://advances.realtimerendering.com/s2014/index.html
float InterleavedGradientNoise(float2 pixCoord, int frameCount)
{
const float3 magic = float3(0.06711056f, 0.00583715f, 52.9829189f);
float2 frameMagicScale = float2(2.083f, 4.867f);
pixCoord += frameCount * frameMagicScale;
return frac(magic.z * frac(dot(pixCoord, magic.xy)));
}

Animated Cross-Fading

如果只用了Cross Fade而没有开启Animated Cross-Fading,那么当摄像机与物体距离正好在两个LOD层级切换的阈值之间时,便会看见物体上的切换Noise:
image.png

显然我们并不希望玩家看清楚这些玩意,而Animated Cross-Fading便是为此而开发。
如果摄像机达到了LOD切换的距离阈值,那么LOD Group就会用半秒(即LODGroup.crossFadeAnimationDuration的默认值)的时间在两个LOD层级之间切换,不会出现玩家停留在某一距离,而LOD切换也停留在一半的情况。

image.png

这节完成时的效果

2021.06.30

Complex Maps - Masks, Details, and Normals

这节就是用上各种Texture,没什么特别值得讲的内容
其中提及的相对陌生一些的内容应当也就是关于纹理压缩格式了

DXT5

image.png
根据压缩格式的不同,采用使用的函数也不一样,DXT5(即BC3)会将原本R通道的值移到A通道(8bits),G通道不变(6bits),其余两通道则为5bits。
image.png

2021.08.09

to be continue


感谢您阅读完本文,若您认为本文对您有所帮助,可以将其分享给其他人;若您发现文章对您的利益产生侵害,请联系作者进行删除。